Skip to content

🐛 selfMetadata 支持空覆盖:修复匹配/排除/标签/运行时机「删除即复活」#1579

Merged
CodFrm merged 4 commits into
mainfrom
fix/settings-match-exclude-reset
Jul 16, 2026
Merged

🐛 selfMetadata 支持空覆盖:修复匹配/排除/标签/运行时机「删除即复活」#1579
CodFrm merged 4 commits into
mainfrom
fix/settings-match-exclude-reset

Conversation

@CodFrm

@CodFrm CodFrm commented Jul 13, 2026

Copy link
Copy Markdown
Member

Checklist / 检查清单

  • Fixes mentioned issues / 修复已提及的问题
  • Code reviewed by human / 代码通过人工检查
  • Changes tested / 已完成测试

Description / 描述

脚本设置里用户对 @match / @exclude / @tag / @run-at 等元数据的覆盖存在 selfMetadata,用来在不改脚本源码的前提下自定义生效规则。

根因selfMetadataUpdatesrc/app/service/service_worker/utils.ts)把「空集合」一律解释为「删除覆盖」,导致后端根本无法表示「用户显式清空」——空覆盖等价于「没有覆盖」,生效值必然回落脚本自带 metadata。因此凡是「删到空」的操作,刷新后规则都会复活,用户永远无法清空脚本自带的规则。

getCombinedMetav ? [...v] : undefined[](真值)本就能正确覆盖,DAO / 备份 / 同步链路也都支持空覆盖(导入 VM/TM 备份时 self_metadata.ts 今天就会写出 match: []),唯独这个函数拒收——这才是本质,所以走根因修复而非继续在前端对齐。

改动

  • selfMetadataUpdate(script, key, Set | undefined)undefined = 撤销覆盖(回落脚本自带 metadata),空集合 = 持久化为 [](显式清空),两者语义不再互相替代。
  • RPC 边界 resetMatch / resetExclude / updateMetadata(+ client.ts)区分 undefined[](原来 new Set(x || []) 把两者塌成同一个空集)。
  • 前端 SettingsPane:重置传 undefined、编辑传数组;「删除最后一项」= 用户显式清空(不再复活脚本自带规则)。
  • 修复 onRunAt 选非 early-start 时下拉框回弹「early-start」的可见 bug(乐观写入 early-start: [],而 runAt真值判断 → [] 被当真值);run-in / run-at 选「默认」改传 undefined 撤销覆盖,行为不变。
  • 重写 8 个语言包的 after_deleting_{match,exclude}_item——旧文案描述的「删除后自动加回」行为已不存在,其中 exclude 那条还自相矛盾(「匹配项…添加到排除项」);新文案描述当前语义:编辑即覆盖、点「重置」恢复脚本自带规则。

覆盖的场景(对应 review 提出的 4 项遗漏路径):

  1. 编辑器逐条删除匹配 / 排除到空 → 显式清空,不再复活。
  2. 编辑器删除最后一个标签 → 不再复活脚本自带 @tag
  3. Popup 取消最后一条排除 → 不再复活脚本自带 @exclude(由根因修复自动受益,excludeUrl 无需改)。
  4. 编辑器选非 early-start 运行时机 → 下拉框不再回弹 early-start。

测试(TDD,先写失败用例再修):

  • 新增 script.test.tsexcludeUrl / resetMatch / resetExclude / updateMetadataundefined vs [] 语义。
  • utils.test.ts:区分「撤销覆盖」与「保存空覆盖」,并修正钉住旧语义的用例。
  • SettingsPane.test.tsx:反转「删除最后一项应回落」用例为「应显式清空」,新增运行时机不回弹 / 选「默认」传 undefined 用例。

受影响套件 + backup / synchronize / runtime / popup 全绿;tsc --noEmit、ESLint、Prettier 均通过。

Screenshots / 截图

N/A(逻辑修复,无 UI 视觉变化)

@cyfung1031

Copy link
Copy Markdown
Collaborator

PR 当前针对“重置匹配规则”的修复方向是正确的。

next === undefined 时,前端会删除对应的覆盖值并回落到脚本原始的 metadata.match;这与后端 selfMetadataUpdate 的行为一致——传入空集合时删除覆盖项。新增的两个测试也覆盖了这条路径,整体实现没有问题。

不过审查时发现,同一根因下还有一条遗漏路径:逐条删除规则,直到列表为空

原来的实现中,删除最后一项后会调用:

setMatchList(kind, [])

这里传给后端的是空数组,而不是 undefined。后端经过 resetMatch(uuid, [])new Set([]) 后,仍会进入 selfMetadataUpdate 的删除逻辑,最终移除覆盖值,使实际生效规则回落到脚本自带的 metadata.match

但前端此时仍显示空列表,并写入 selfMetadata[kind] = [],导致前后端状态不一致。用户会以为脚本已经不再匹配任何网站,实际上脚本仍然按照原始的 @match 规则运行;页面刷新后,这些规则还会再次“复活”。

已在 SettingsPane.tsx 中将空数组和 undefined 统一处理:

const override = next?.length ? next : undefined

这样,无论是点击重置,还是逐条删除到空列表,都会走相同的“删除覆盖并回落原始 metadata”逻辑。

修复过程按照仓库的 TDD 规范进行:先补充失败用例,复现“删除最后一项后前端显示为空,但后端已经回落原始规则”的问题,再修改实现。新增测试为:

删除最后一项匹配后,应回落脚本自带规则,而不是显示为空列表。


另外还发现几处同类问题,但与本 PR 的修改范围关系较弱,因此没有一并处理,只建议后续单独跟进:

  1. 删除最后一个标签

    commitTags([]) 会调用后端 updateMetadata("tag", [])。空数组同样会导致 selfMetadata.tag 覆盖值被删除,刷新后脚本原始的 metadata.tag 会重新出现,而前端在刷新前仍显示为空。

  2. Popup 中删除最后一条排除规则

    script.ts:729 附近的 excludeUrl 也存在类似情况。删除最后一条 exclude 后,覆盖值会被移除,最终回落到原始的 metadata.exclude

    这类问题的根本原因是当前数据模型无法区分:

    • 没有设置覆盖值,使用脚本原始 metadata
    • 明确设置了一个空列表,表示不使用任何规则

    若要彻底解决,需要后端支持保存“空覆盖”,而不是把空集合统一解释为删除覆盖值。

  3. 中文文案已过时

    zh-CN/editor.json:69-70 中,after_deleting_match_itemafter_deleting_exclude_item 描述的行为与当前实现不一致,其中一条文案本身也存在语义矛盾,建议另开 PR 清理。

  4. run-in / run-at 的空数组存储差异

    这两项也存在前后端对 [] 处理不一致的情况,不过当前显示逻辑使用 ?.[0],空数组与未定义在界面上的表现相同,暂时没有可见影响,不需要在本 PR 中调整。

@cyfung1031

Copy link
Copy Markdown
Collaborator

@CodFrm 那4个交给你了

根因:selfMetadataUpdate 把空集合一律当成「删除覆盖」,导致用户永远无法
显式清空脚本自带的 tag / match / exclude——删到空后覆盖被删、生效值回落
metadata,刷新即复活。改为区分 undefined(撤销覆盖) 与空集合(显式清空):

- selfMetadataUpdate 接受 Set|undefined,空集合持久化为 []
- resetMatch/resetExclude/updateMetadata RPC 边界区分 undefined 与 []
  (excludeUrl 由根因修复自动受益,popup 取消最后一条排除不再复活)
- SettingsPane 前端对齐:重置传 undefined、编辑传数组;删除最后一项 = 显式清空
- 修复 onRunAt 选非 early-start 时下拉框回弹 early-start(空数组被当真值);
  run-in/run-at 选「默认」改传 undefined 撤销覆盖,保持行为不变
- 重写 8 个语言包 after_deleting_{match,exclude}_item 过时且自相矛盾的文案

对应 review 提出的 4 项遗漏路径(标签/popup 排除/文案/run-at)
@CodFrm

CodFrm commented Jul 16, 2026

Copy link
Copy Markdown
Member Author

4 项已在本 PR 一并处理,思路是走根因而非继续在前端对齐:

selfMetadataUpdate 把「空集合」一律解释为「删除覆盖」,所以后端根本无法表示「用户显式清空」——空覆盖等于没有覆盖,生效值必然回落脚本自带 metadata。这正是第 1、2 项的本质(不只是前端显示不一致,而是功能做不到:标签/排除删到空后刷新必复活)。

改动:

  • selfMetadataUpdate(script, key, Set | undefined)undefined = 撤销覆盖(回落脚本自带),空集合 = 持久化为 [](显式清空)。getCombinedMetav ? [...v] : undefined[](真值)本就正确覆盖,导入 VM/TM 备份时 self_metadata.ts 今天也已能写出 match: [],所以整条 DAO/备份/同步链路都支持空覆盖,唯独这个函数拒收。
  • RPC 边界 resetMatch/resetExclude/updateMetadata 区分 undefined[](原来 new Set(x || []) 把两者塌成同一个空集)。第 2 项 popup 取消排除由此自动修好,excludeUrl 无需改。
  • 第 4 项顺带发现一个可见 bug:onRunAt 选非 early-start 时乐观写入 early-start: [],而 runAt 用的是真值判断(不是你说的 ?.[0]),[] 为真 → 下拉框回弹「early-start」。且根因修复后 [] 会被持久化使其跨刷新固化,所以这里改成传 undefined 撤销覆盖;run-in/run-at 选「默认」同理改传 undefined,行为不变。
  • 第 3 项:重写 8 个语言包的 after_deleting_{match,exclude}_item——旧文案描述的「删除后自动加回」行为已不存在,其中 exclude 那条还自相矛盾(「匹配项…添加到排除项」)。新文案描述当前语义:编辑即覆盖、点「重置」恢复脚本自带规则。

⚠️ 一处反转了你在 6e40df3 加的用例「删除最后一项匹配后应回落脚本自带规则」:新语义下删到空 = 用户显式清空,脚本自带规则不应复活(否则用户永远删不掉 @match/@exclude)。与你评论里「彻底解决需后端支持保存空覆盖」的判断一致,前端对齐那版作为 stopgap 被这版取代。

测试:新增 script.test.ts(excludeUrl/resetMatch/resetExclude/updateMetadata 的 undefined vs [] 语义)+ utils.test.ts/SettingsPane.test.tsx 补齐;受影响套件 + backup/sync/runtime/popup 全绿,tsc/eslint/prettier 通过。

@CodFrm CodFrm changed the title 🐛 修复网站匹配/排除重置后清空列表(应回落脚本自带规则) 🐛 selfMetadata 支持空覆盖:修复匹配/排除/标签/运行时机「删除即复活」 Jul 16, 2026
@CodFrm CodFrm merged commit 550c0b1 into main Jul 16, 2026
9 of 10 checks passed
@CodFrm CodFrm deleted the fix/settings-match-exclude-reset branch July 16, 2026 08:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants